<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Three.js 体积渲染热力图</title>
    <style>
        body { margin: 0; overflow: hidden; }
        canvas { display: block; }
        #info {
            position: absolute;
            top: 10px;
            width: 100%;
            text-align: center;
            color: white;
            font-family: Arial, sans-serif;
            pointer-events: none;
        }
    </style>
</head>
<body>
    <div id="info">Three.js 体积渲染热力图<br>鼠标拖动旋转，滚轮缩放</div>
    <script src="https://cdn.jsdelivr.net/npm/three@0.132.2/build/three.min.js"></script>
    <script src="https://cdn.jsdelivr.net/npm/three@0.132.2/examples/js/controls/OrbitControls.js"></script>
    <script src="https://cdn.jsdelivr.net/npm/dat.gui@0.7.7/build/dat.gui.min.js"></script>
    <script>
        // 初始化场景、相机和渲染器
        const scene = new THREE.Scene();
        scene.background = new THREE.Color(0x111111);
        
        const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
        camera.position.z = 2.5;
        
        const renderer = new THREE.WebGLRenderer({ antialias: true });
        renderer.setSize(window.innerWidth, window.innerHeight);
        renderer.setPixelRatio(window.devicePixelRatio);
        document.body.appendChild(renderer.domElement);
        
        // 添加轨道控制器
        const controls = new THREE.OrbitControls(camera, renderer.domElement);
        controls.enableDamping = true;
        controls.dampingFactor = 0.05;
        
        // 窗口大小调整
        window.addEventListener('resize', () => {
            camera.aspect = window.innerWidth / window.innerHeight;
            camera.updateProjectionMatrix();
            renderer.setSize(window.innerWidth, window.innerHeight);
        });
        
        // 创建3D热力数据
        const createHeatData = (size) => {
            const data = new Float32Array(size * size * size);
            
            // 创建几个球形热源
            const spheres = [
                { center: [0.3, 0.3, 0.3], radius: 0.2, intensity: 1.0 },
                { center: [0.7, 0.7, 0.3], radius: 0.15, intensity: 0.8 },
                { center: [0.5, 0.5, 0.7], radius: 0.25, intensity: 1.2 },
                { center: [0.2, 0.8, 0.5], radius: 0.18, intensity: 0.9 },
                { center: [0.8, 0.2, 0.5], radius: 0.22, intensity: 1.1 }
            ];
            
            for (let z = 0; z < size; z++) {
                for (let y = 0; y < size; y++) {
                    for (let x = 0; x < size; x++) {
                        const nx = x / (size - 1);  // 归一化到[0,1]
                        const ny = y / (size - 1);
                        const nz = z / (size - 1);
                        
                        let density = 0;
                        
                        // 计算每个球体的贡献
                        for (const sphere of spheres) {
                            const dx = nx - sphere.center[0];
                            const dy = ny - sphere.center[1];
                            const dz = nz - sphere.center[2];
                            const distance = Math.sqrt(dx*dx + dy*dy + dz*dz);
                            
                            if (distance < sphere.radius) {
                                // 使用平滑衰减函数
                                const falloff = Math.pow(1.0 - distance/sphere.radius, 2);
                                density += sphere.intensity * falloff;
                            }
                        }
                        
                        // 添加一些噪声
                        const noise = 0.05 * Math.random();
                        data[z * size * size + y * size + x] = Math.min(1.0, density + noise);
                    }
                }
            }
            
            return data;
        };
        
        const volumeSize = 64;  // 提高分辨率以获得更好效果
        const heatData = createHeatData(volumeSize);
        
        // 创建3D纹理
        const dataTexture = new THREE.DataTexture3D(
            heatData, 
            volumeSize, volumeSize, volumeSize
        );
        dataTexture.format = THREE.RedFormat;
        dataTexture.type = THREE.FloatType;
        dataTexture.minFilter = THREE.LinearFilter;
        dataTexture.magFilter = THREE.LinearFilter;
        dataTexture.unpackAlignment = 1;
        dataTexture.needsUpdate = true;
        
        // 着色器代码
        const vertexShader = `
            varying vec3 vPosition;
            varying vec3 vWorldPosition;
            varying vec4 vProjectedPosition;
            
            void main() {
                vPosition = position;
                vWorldPosition = (modelMatrix * vec4(position, 1.0)).xyz;
                vProjectedPosition = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
                gl_Position = vProjectedPosition;
            }
        `;
        
        const fragmentShader = `
            precision highp float;
            precision highp sampler3D;
            
            uniform sampler3D volumeTexture;
            uniform float threshold;
            uniform int steps;
            uniform float stepSize;
            uniform float densityScale;
            uniform vec3 boundsMin;
            uniform vec3 boundsMax;
            
            varying vec3 vPosition;
            varying vec3 vWorldPosition;
            varying vec4 vProjectedPosition;
            
            // 热图颜色映射
            vec4 getHeatColor(float value) {
                if (value < 0.25) return vec4(0.0, 0.0, 0.5 + value, value * 0.5);
                else if (value < 0.5) return vec4(0.0, (value - 0.25) * 4.0, 1.0, value * 0.7);
                else if (value < 0.75) return vec4((value - 0.5) * 4.0, 1.0, 1.0 - (value - 0.5) * 4.0, value * 0.9);
                else return vec4(1.0, 1.0 - (value - 0.75) * 4.0, 0.0, value);
            }
            
            // 计算射线与AABB盒的交点
            bool intersectBox(vec3 rayOrigin, vec3 rayDir, vec3 boxMin, vec3 boxMax, 
                            out float tMin, out float tMax) {
                vec3 invDir = 1.0 / rayDir;
                vec3 tBottom = (boxMin - rayOrigin) * invDir;
                vec3 tTop = (boxMax - rayOrigin) * invDir;
                vec3 tMin3 = min(tBottom, tTop);
                vec3 tMax3 = max(tBottom, tTop);
                tMin = max(max(tMin3.x, tMin3.y), tMin3.z);
                tMax = min(min(tMax3.x, tMax3.y), tMax3.z);
                return tMin <= tMax;
            }
            
            void main() {
                // 计算视线方向
                vec3 rayDir = normalize(vWorldPosition - cameraPosition);
                
                // 计算射线与体积边界的交点
                float tEnter, tExit;
                bool hit = intersectBox(cameraPosition, rayDir, boundsMin, boundsMax, tEnter, tExit);
                
                // 如果没有击中体积，使用绿色背景
                if (!hit) {
                    gl_FragColor = vec4(0.0, 0.5, 0.0, 1.0);
                    return;
                }
                
                // 确保我们从相机前方开始
                tEnter = max(tEnter, 0.0);
                
                // 计算射线起点和终点
                vec3 rayStart = cameraPosition + rayDir * tEnter;
                vec3 rayEnd = cameraPosition + rayDir * tExit;
                
                // 计算实际步数和步长
                float rayLength = distance(rayStart, rayEnd);
                int actualSteps = int(min(float(steps), rayLength / stepSize));
                float actualStepSize = rayLength / float(actualSteps);
                
                // 初始化颜色和透明度
                vec4 finalColor = vec4(0.0, 0.5, 0.0, 1.0); // 初始为绿色背景
                float accumulatedAlpha = 0.0;
                
                // Raymarching循环
                for (int i = 0; i < actualSteps; i++) {
                    float t = float(i) * actualStepSize;
                    vec3 samplePos = rayStart + rayDir * t;
                    
                    // 转换到纹理坐标[0,1]
                    vec3 texCoord = (samplePos - boundsMin) / (boundsMax - boundsMin);
                    
                    // 采样体积纹理
                    float density = texture(volumeTexture, texCoord).r * densityScale;
                    
                    if (density > threshold) {
                        vec4 sampleColor = getHeatColor(density);
                        
                        // 前积混合
                        sampleColor.a *= 0.1; // 控制透明度
                        sampleColor.rgb *= sampleColor.a;
                        finalColor = finalColor + sampleColor * (1.0 - accumulatedAlpha);
                        accumulatedAlpha += sampleColor.a * (1.0 - accumulatedAlpha);
                        
                        // 提前终止
                        if (accumulatedAlpha > 0.95) break;
                    }
                }
                
                // 应用一些后期效果
                finalColor.rgb = pow(finalColor.rgb, vec3(1.0/2.2)); // Gamma校正
                gl_FragColor = finalColor;
            }
        `;
        
        // 创建着色器材质
        const shaderMaterial = new THREE.ShaderMaterial({
            uniforms: {
                volumeTexture: { value: dataTexture },
                threshold: { value: 0.05 },
                steps: { value: 256 },
                stepSize: { value: 0.005 },
                densityScale: { value: 1.0 },
                boundsMin: { value: new THREE.Vector3(-0.5, -0.5, -0.5) },
                boundsMax: { value: new THREE.Vector3(0.5, 0.5, 0.5) }
            },
            vertexShader: vertexShader,
            fragmentShader: fragmentShader,
            side: THREE.BackSide, // 重要：从内部渲染
            transparent: true,
            blending: THREE.NormalBlending
        });
        
        // 创建立方体
        const cubeGeometry = new THREE.BoxGeometry(1, 1, 1);
        const cube = new THREE.Mesh(cubeGeometry, shaderMaterial);
        scene.add(cube);
        
        // 添加坐标轴辅助
        const axesHelper = new THREE.AxesHelper(1.5);
        scene.add(axesHelper);
        
        // 添加GUI控制
        const gui = new dat.GUI();
        gui.add(shaderMaterial.uniforms.threshold, 'value', 0, 0.2).name('阈值').step(0.001);
        gui.add(shaderMaterial.uniforms.densityScale, 'value', 0.1, 3).name('密度比例').step(0.1);
        gui.add(shaderMaterial.uniforms.steps, 'value', 32, 512).name('光线步数');
        gui.add(shaderMaterial.uniforms.stepSize, 'value', 0.001, 0.02).name('步长').step(0.001);
        
        // 动画循环
        function animate() {
            requestAnimationFrame(animate);
            controls.update();
            renderer.render(scene, camera);
        }
        
        animate();
    </script>
</body>
</html>